package com.qcrud.core.transaction;

import com.qcrud.core.Handle;
import com.qcrud.core.HandleCallback;
import com.qcrud.exception.Unchecked;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

public class LocalTransactionHandler implements TransactionHandler {
    private final Map<Handle, BoundLocalTransactionHandler> bound = Collections.synchronizedMap(new WeakHashMap<>());

    @Override
    public void begin(Handle handle) {
        getHandle(handle).begin(handle);
    }

    @Override
    public void commit(Handle handle) {
        getHandle(handle).commit(handle);
    }

    @Override
    public void rollback(Handle handle) {
        getHandle(handle).rollback(handle);
    }

    @Override
    public boolean isInTransaction(Handle handle) {
        return getHandle(handle).isInTransaction(handle);
    }

    @Override
    public void savepoint(Handle handle, String savepointName) {
        getHandle(handle).savepoint(handle, savepointName);
    }

    @Override
    public void rollbackToSavepoint(Handle handle, String savepointName) {
        getHandle(handle).rollbackToSavepoint(handle, savepointName);
    }

    @Override
    public void releaseSavepoint(Handle handle, String savepointName) {
        getHandle(handle).releaseSavepoint(handle, savepointName);
    }

    @Override
    public <R, X extends Exception> R inTransaction(Handle handle, HandleCallback<R, X> callback) throws X {
        return getHandle(handle).inTransaction(handle, callback);
    }

    @Override
    public <R, X extends Exception> R inTransaction(Handle handle, TransactionIsolationLevel level, HandleCallback<R, X> callback) throws X {
        return getHandle(handle).inTransaction(handle, level, callback);
    }

    protected TransactionHandler getHandle(Handle handle) {
        return bound.computeIfAbsent(handle, Unchecked.function(BoundLocalTransactionHandler::new));
    }

    public static LocalTransactionHandler binding() {
        return new BindingLocalTransactionHandler();
    }

    static class BindingLocalTransactionHandler extends LocalTransactionHandler {
        @Override
        public TransactionHandler specialize(Handle handle) throws SQLException {
            return new BoundLocalTransactionHandler(handle);
        }
    }

    static class BoundLocalTransactionHandler implements TransactionHandler {
        private final Map<String, Savepoint> savepointMap = new HashMap<>();
        private boolean initialAutocommit;
        private boolean didBegin;
        private boolean didRollback;

        BoundLocalTransactionHandler(Handle handle) throws SQLException {
            this.initialAutocommit = handle.getConnection().getAutoCommit();
        }

        @Override
        public void begin(Handle handle) {
            try {
                if (!didBegin) {
                    Connection conn = handle.getConnection();
                    initialAutocommit = conn.getAutoCommit();
                    didRollback = false;
                    savepointMap.clear();
                    conn.setAutoCommit(false);
                    didBegin = true;
                }
            } catch (SQLException e) {
                throw new TransactionException("Failed to start transaction", e);
            }
        }

        @Override
        public void commit(Handle handle) {
            try {
                handle.getConnection().commit();
            } catch (SQLException e) {
                throw new TransactionException("Failed to commit transaction", e);
            } finally {
                restoreAutoCommitState(handle);
            }
        }

        @Override
        public void rollback(Handle handle) {
            try {
                didRollback = true;
                handle.getConnection().rollback();
            } catch (SQLException e) {
                throw new TransactionException("Failed to rollback transaction", e);
            } finally {
                restoreAutoCommitState(handle);
            }
        }

        @Override
        public void savepoint(Handle handle, String name) {
            final Connection conn = handle.getConnection();
            try {
                final Savepoint savepoint = conn.setSavepoint(name);
                savepointMap.put(name, savepoint);
            } catch (SQLException e) {
                throw new TransactionException(String.format("Unable to create savepoint '%s'", name), e);
            }
        }

        @Override
        public void releaseSavepoint(Handle handle, String name) {
            final Connection conn = handle.getConnection();
            try {
                final Savepoint savepoint = savepointMap.remove(name);
                if (savepoint == null) {
                    throw new TransactionException(String.format("Attempt to release non-existent savepoint, '%s'",
                            name));
                }
                conn.releaseSavepoint(savepoint);
            } catch (SQLException e) {
                throw new TransactionException(String.format("Unable to create savepoint %s", name), e);
            }
        }

        @Override
        public void rollbackToSavepoint(Handle handle, String name) {
            final Connection conn = handle.getConnection();
            try {
                final Savepoint savepoint = savepointMap.remove(name);
                if (savepoint == null) {
                    throw new TransactionException(String.format("Attempt to rollback to non-existent savepoint, '%s'",
                            name));
                }
                conn.rollback(savepoint);
            } catch (SQLException e) {
                throw new TransactionException(String.format("Unable to create savepoint %s", name), e);
            }
        }

        @Override
        public boolean isInTransaction(Handle handle) {
            try {
                return !handle.getConnection().getAutoCommit();
            } catch (SQLException e) {
                throw new TransactionException("Failed to test for transaction status", e);
            }
        }

        @Override
        public <R, X extends Exception> R inTransaction(Handle handle,
                                                        HandleCallback<R, X> callback) throws X {
            if (isInTransaction(handle)) {
                throw new IllegalStateException("Already in transaction");
            }
            didRollback = false;
            final R returnValue;
            try {
                // 开启事务
                handle.begin();
                returnValue = callback.withHandle(handle);
                if (!didRollback) {
                    // 提交事务
                    handle.commit();
                }
            } catch (Throwable e) {
                try {
                    // 事务回滚
                    handle.rollback();
                } catch (Exception rollback) {
                    e.addSuppressed(rollback);
                }
                throw e;
            } finally {
                didRollback = false;
            }
            return returnValue;
        }

        @Override
        public <R, X extends Exception> R inTransaction(Handle handle,
                                                        TransactionIsolationLevel level,
                                                        HandleCallback<R, X> callback) throws X {
            final TransactionIsolationLevel initial = handle.getTransactionIsolationLevel();
            try {
                handle.setTransactionIsolation(level);
                return inTransaction(handle, callback);
            } finally {
                handle.setTransactionIsolation(initial);
            }
        }

        private void restoreAutoCommitState(Handle handle) {
            try {
                if (initialAutocommit) {
                    handle.getConnection().setAutoCommit(initialAutocommit);
                    savepointMap.clear();
                    didBegin = false;
                }
            } catch (SQLException e) {
                throw new UnableToRestoreAutoCommitStateException(e);
            }
        }
    }
}